/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { AliasType, AnyType, ArkArrayRef, ArkAssignStmt, ArkCaughtExceptionRef, ArkClass, ArkField, ArkFile, ArkInstanceFieldRef, ArkInstanceInvokeExpr, ArkMethod, ArkNormalBinopExpr, ArkParameterRef, ArkPtrInvokeExpr, ArkStaticFieldRef, ArkStaticInvokeExpr, ArkThisRef, ArkUnopExpr, AstTreeUtils, Local, Stmt, ts, Type, UnknownType, Value } from "arkanalyzer";
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { FileMatcher, MatcherCallback, MatcherTypes } from "../../matcher/Matchers";
import { Defects, IssueReport } from "../../model/Defects";
import { Rule } from "../../model/Rule";
import { isAppointType } from "../../utils/checker/TypeUtils";
import { RuleListUtil } from "../../utils/common/DefectsList";
import { BaseChecker, BaseMetaData } from "../BaseChecker";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'NoUnsafeAssignmentCheck');
const gMetaData: BaseMetaData = {
  severity: 2,
  ruleDocPath: "docs/no-unsafe-assignment-check.md",
  description: "Disallow assigning a value with type `any` to variables and properties.",
};

export class NoUnsafeAssignmentCheck implements BaseChecker {
  public defects: Defects[] = [];
  public issues: IssueReport[] = [];
  readonly metaData: BaseMetaData = gMetaData;
  public rule: Rule;
  private fileMatcher: FileMatcher = {
    matcherType: MatcherTypes.FILE,
  };

  public registerMatchers(): MatcherCallback[] {
    const fileMatcherCb: MatcherCallback = {
      matcher: this.fileMatcher,
      callback: this.check
    }
    return [fileMatcherCb];
  }

  public check = (target: ArkFile) => {

    const originPath = target.getName();
    if (!this.isTsFile(originPath)) {
      return;
    }

    const mClasses: ArkClass[] = target.getClasses();
    mClasses.forEach((clazz: ArkClass) => {
      const mFields = clazz.getFields();
      this.checkFields(mFields, target);

      const mMethods = clazz.getMethods();
      this.checkMethod(mMethods);
    });
  }

  private checkMethod(mMethods: ArkMethod[]): void {
    mMethods.forEach((method: ArkMethod) => {
      const blocks = method.getCfg()?.getBlocks();
      if (!blocks) {
        return;
      }
      blocks.forEach((block) => {
        const stmts = block.getStmts();
        this.checkStmts(stmts);
      });
    });
  }

  private checkStmts(stmts: Stmt[]): void {
    stmts.forEach((stmt) => {
      if (!(stmt instanceof ArkAssignStmt)) {
        return;
      }
      const originalText = stmt.getOriginalText();
      if (originalText?.includes('catch')) {
        return;
      }
      const leftOp = stmt.getLeftOp();
      if (this.shouldSaveError(stmt) && leftOp instanceof Local) {
        this.saveError(stmt, leftOp.getName());
      }
    });
  }

  private shouldSaveError(stmt: ArkAssignStmt): boolean {
    const rightOp = stmt.getRightOp();
    const leftOp = stmt.getLeftOp();
    if (!(leftOp instanceof Local) || leftOp.getName().startsWith('%') || this.isExcludedRightOp(rightOp)) {
      return false;
    }

    if (this.isRightAnyOrUnknow(stmt, leftOp.getName())) {
      return true;
    }

    if (rightOp instanceof Local && rightOp.getName() === 'this') {
      return true;
    }

    if (rightOp instanceof ArkArrayRef && this.isRightAnyOrUnknow(stmt)) {
      return true;
    }

    if (leftOp.getType() && !(leftOp.getType() instanceof AliasType) &&
      this.isAnyType(leftOp.getType()) &&
      rightOp instanceof ArkParameterRef) {
      const code = stmt.getCfg()?.getDeclaringMethod().getCode();
      if (code && this.checkMethodByAst(AstTreeUtils.getASTNode('', code))) {
        return true;
      }
    }

    if (rightOp instanceof ArkInstanceFieldRef) {
      return this.isAnyType(this.getType(rightOp)) && this.isRightAnyOrUnknow(stmt, leftOp.getName());
    }

    if (rightOp instanceof ArkStaticInvokeExpr && !this.isArkStaticInvokeExprSafe(rightOp)) {
      return true;
    }

    return this.isAnyType(this.getType(rightOp));
  }
  
  private isExcludedRightOp(rightOp: Value): boolean {
    return (
      rightOp instanceof ArkPtrInvokeExpr ||
      rightOp instanceof ArkCaughtExceptionRef ||
      rightOp instanceof ArkUnopExpr ||
      rightOp instanceof ArkInstanceInvokeExpr ||
      rightOp instanceof ArkNormalBinopExpr ||
      rightOp instanceof ArkThisRef
    );
  }

  private checkFields(mFields: ArkField[], target: ArkFile): void {
    mFields.forEach((field: ArkField) => {
      const signature = field.getSignature();
      if (!signature) {
        return;
      }
      const fieldName = signature.getFieldName();
      const fieldInitializer = field.getInitializer();
      const fieldCode = field.getCode();
      if (fieldCode && fieldInitializer.length <= 0 &&
        this.checkFieldByAst(AstTreeUtils.getASTNode('', fieldCode))) {
        const pos = {
          startCol: field.getOriginPosition().getColNo(),
          endCol: field.getOriginPosition().getColNo() + fieldName.length,
          line: field.getOriginPosition().getLineNo(),
          filePath: target.getName()
        };
        if (pos) {
          const message = 'Unsafe assignment of an `any` value.';
          const severity = this.rule.alert ?? this.metaData.severity;
          this.addIssueReport(pos, severity, message);
        }
      }

      if (fieldInitializer.length <= 0) {
        return;
      }
      fieldInitializer.forEach((stmt: Stmt) => {
        if (stmt instanceof ArkAssignStmt &&
          (stmt.getLeftOp() instanceof ArkInstanceFieldRef || stmt.getLeftOp() instanceof ArkStaticFieldRef) && 
          (this.isAnyType(this.getType(stmt.getRightOp())) && this.isRightAnyOrUnknow(stmt, fieldName))) {
            this.saveError(stmt, fieldName);
        }
      });
    });
  }

  private getType(value: Value): Type | undefined {
    if (value instanceof Local) {
      return value.getType();
    } else if (value instanceof ArkArrayRef) {
      return value.getBase().getType();
    } else if (value instanceof ArkInstanceFieldRef) {
      if (value.getFieldSignature() && value.getFieldSignature().getType()) {
        return value.getFieldSignature().getType();
      }
    }
    return undefined;
  }

  private isArkStaticInvokeExprSafe(data: ArkStaticInvokeExpr): boolean {
    let isSafe = true;
    const args = data.getArgs();
    args.forEach((arg) => {
      if (arg.getType() && this.isAnyType(arg.getType())) {
        isSafe = false;
      }
    });
    const methodInfo = data.getMethodSignature()?.getMethodSubSignature();
    if (methodInfo && methodInfo.getReturnType() && this.isAnyType(methodInfo.getReturnType())) {
      isSafe = false;
    }
    return isSafe;
  }

  private checkFieldByAst(sNode: ts.SourceFile): boolean {
    let isAny = false;
    const visitParam = (node: ts.Node): void => {
      // 检查函数参数默认值
      if (node.kind === ts.SyntaxKind.AnyKeyword || node.kind === ts.SyntaxKind.UnknownKeyword) {
        isAny = true;
        return;
      }

      // 递归遍历子节点
      ts.forEachChild(node, visitParam);
    };
    ts.forEachChild(sNode, visitParam);
    return isAny;
  }

  private checkMethodByAst(sNode: ts.SourceFile): boolean {
    let isAny = false;
    const methodNode = sNode.statements[0];
    if (
      ts.isFunctionDeclaration(methodNode) ||
      ts.isArrowFunction(methodNode) ||
      ts.isMethodDeclaration(methodNode) ||
      ts.isFunctionExpression(methodNode)
    ) {
      const params = methodNode.parameters;
      isAny = this.checkMethodParamsIsAny(params, isAny);
    }
    return isAny;
  }

  private checkMethodParamsIsAny(params: ts.NodeArray<ts.ParameterDeclaration>, isAny: boolean): boolean {
    params.forEach((param) => {
      const initializer = param.initializer as ts.Node;
      if (!initializer) {
        return;
      }
      if (ts.isAsExpression(initializer) && initializer.type &&
        (initializer.type.kind === ts.SyntaxKind.AnyKeyword ||
          initializer.type.kind === ts.SyntaxKind.UnknownKeyword)) {
        isAny = true;
      } else if (initializer.kind === ts.SyntaxKind.AnyKeyword ||
        initializer.kind === ts.SyntaxKind.UnknownKeyword) {
        isAny = true;
      }
    });
    return isAny;
  }

  private isRightAnyOrUnknow(stmt: Stmt, leftText: string = ''): boolean {
    let isAny = false;
    try {
      let code = stmt.getOriginalText();
      if (!code) {
        code = stmt.getCfg()?.getDeclaringMethod().getCode();
      }
      if (!code) {
        code = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkClass().getCode();
      }
      if (!code) {
        return false;
      }
      const sNode = AstTreeUtils.getASTNode('', code);
      isAny = this.checkMethodParams(leftText, isAny, sNode);
    } catch (error) {
      logger.error(error);
    }
    return isAny;
  }

  private checkMethodParams(leftText: string, isAny: boolean, sNode: ts.SourceFile): boolean {
    const visitParam = (item: ts.Node): void => {
      // 检查函数参数默认值
      if (ts.isBinaryExpression(item)) {
        if (leftText !== '' && item.left && item.left.getText() !== leftText) {
          return;
        }
        if (item.right &&
          (item.right.kind === ts.SyntaxKind.AnyKeyword ||
            item.right.kind === ts.SyntaxKind.UnknownKeyword)) {
          isAny = true;
          return;
        }
      } else if (ts.isBindingElement(item) && item.initializer) {
        const name = item.name.getText();
        if (leftText !== '' && name !== leftText) {
          return;
        }
        if (item.initializer.kind === ts.SyntaxKind.AnyKeyword ||
          item.initializer.kind === ts.SyntaxKind.UnknownKeyword) {
          isAny = true;
          return;
        }
      } else if (ts.isAsExpression(item)) {
        if (item.type &&
          (item.type.kind === ts.SyntaxKind.AnyKeyword ||
            item.type.kind === ts.SyntaxKind.UnknownKeyword)) {
          isAny = true;
          return;
        }
      }

      // 递归遍历子节点
      ts.forEachChild(item, visitParam);
    };
    ts.forEachChild(sNode, visitParam);
    return isAny;
  }

  private isArkAssignStmtHaseRightOp(stmt: ArkAssignStmt): boolean {
    const ops = stmt.getOperandOriginalPositions();
    if (!ops || ops.length < 2) {
      return false;
    }
    if (ops[1].getFirstLine() === -1) {
      return false;
    }
    return true;
  }

  private saveError(stmt: ArkAssignStmt, leftText?: string): void {
    const pos = this.getLineAndColumn(stmt, leftText);
    if (pos) {
      const message = 'Unsafe assignment of an `any` value.';
      const severity = this.rule.alert ?? this.metaData.severity;
      this.addIssueReport(pos, severity, message);
    }
  }

  private isAnyType(type: Type | undefined): boolean {
    if (!type) {
      return false;
    }
    return isAppointType(AnyType.getInstance(), type) || isAppointType(UnknownType.getInstance(), type);
  }

  private addIssueReport(pos: { line: number, startCol: number, endCol: number, filePath: string }, severity: number, message: string) {
    let defects = new Defects(pos.line, pos.startCol, pos.endCol, message, severity, this.rule.ruleId,
      pos.filePath, this.metaData.ruleDocPath, true, false, false);
    this.issues.push(new IssueReport(defects, undefined));
    RuleListUtil.push(defects);
  }

  private getLineAndColumn(stmt: Stmt, leftText: string = ''): { line: number, startCol: number, endCol: number, filePath: string } | undefined {

    const arkFile = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile();
    let line = 0;
    let startCol = 0;
    let endCol = 0;

    if (stmt.getOriginalText()) {
      if (stmt instanceof ArkAssignStmt &&
        (stmt.getLeftOp() instanceof ArkInstanceFieldRef ||
          stmt.getLeftOp() instanceof ArkStaticFieldRef)) {
        line = stmt.getOriginPositionInfo().getLineNo();
        startCol = stmt.getOriginPositionInfo().getColNo();
        endCol = startCol + leftText.length;
      } else {
        const positions = stmt.getOperandOriginalPositions();
        if (!positions || positions.length < 1) {
          logger.debug('positions is empty');
          return {
            line: -1,
            startCol: -1,
            endCol: -1,
            filePath: ''
          };
        }
        let index = 0;
        if (stmt instanceof ArkAssignStmt && stmt.getRightOp() instanceof ArkArrayRef) {
          index = 1;
        }
        line = positions[index].getFirstLine();
        startCol = positions[index].getFirstCol();
        endCol = positions[index].getLastCol();
      }
    } else if (stmt.getCfg()?.getDeclaringMethod().getCode()) {
      ({ line, startCol, endCol } = this.getLineAndColFromMethodCode(line, stmt, startCol, endCol, leftText));
    } else if (stmt.getCfg().getDeclaringMethod().getDeclaringArkClass().getCode()) {
      ({ line, startCol, endCol } = this.getLineAndColFromClassCode(line, stmt, startCol, endCol, leftText));
    }
    return {
      line: line,
      startCol: startCol,
      endCol: endCol,
      filePath: arkFile.getName()
    };
  }

  private getLineAndColFromMethodCode(line: number, stmt: Stmt, startCol: number, endCol: number, leftText: string): 
    { line: number, startCol: number, endCol: number } {
    line = stmt.getCfg()?.getDeclaringMethod().getLine() ?? 0;

    const node = AstTreeUtils.getASTNode('', stmt.getCfg()?.getDeclaringMethod().getCode() ?? '');
    const { startCol: nodeStart, endCol: nodeEnd } = this.getColFromAstNode(node, leftText);
    const codeStartCol = stmt.getCfg()?.getDeclaringMethod().getColumn() ?? 1;
    startCol = codeStartCol + nodeStart;
    endCol = codeStartCol + nodeEnd;
    return { line, startCol, endCol };
  }

  private getLineAndColFromClassCode(line: number, stmt: Stmt, startCol: number, endCol: number, leftText: string): 
    { line: number, startCol: number, endCol: number } {
    line = stmt.getCfg().getDeclaringMethod().getDeclaringArkClass().getLine();

    const node = AstTreeUtils.getASTNode('', stmt.getCfg().getDeclaringMethod().getDeclaringArkClass().getCode() ?? '');
    const { startCol: nodeStart, endCol: nodeEnd } = this.getColFromAstNode(node, leftText);
    const codeStartCol = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkClass().getColumn() ?? 1;
    startCol = codeStartCol + nodeStart;
    endCol = codeStartCol + nodeEnd;
    return { line, startCol, endCol };
  }

  private getColFromAstNode(cNode: ts.SourceFile, key: string): { startCol: number, endCol: number } {
    let startCol = 0;
    let endCol = 0;
    const visitNode = (node: ts.Node): void => {
      if (node.getText() === key) {
        startCol = node.getStart();
        endCol = node.getEnd();
        return;
      }
      ts.forEachChild(node, visitNode);
    };
    ts.forEachChild(cNode, visitNode);
    return { startCol, endCol };
  }

  private isTsFile(filePath: string): boolean {
    return filePath.toLowerCase().endsWith('.ts');
  }
}
